package com.zhuhjay.security.filter;

import cn.hutool.core.util.StrUtil;
import com.zhuhjay.constant.RedisConstant;
import com.zhuhjay.constant.SecurityConstant;
import com.zhuhjay.enums.HttpCode;
import com.zhuhjay.security.LoginUserDetail;
import com.zhuhjay.utils.*;
import com.zhuhjay.utils.result.ResultBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.zhuhjay.constant.RedisConstant.TOKEN_PREFIX;
import static com.zhuhjay.constant.RedisConstant.UID_PREFIX;
import static com.zhuhjay.constant.SecurityConstant.AUTH_HEADER_KEY;
import static com.zhuhjay.utils.JwtUtils.EXPIRATION_TIME;
import static com.zhuhjay.utils.JwtUtils.TIME_UNIT;

/**
 * 此过滤器会自动解析HTTP请求中头部名字为Authentication，且以Basic开头的头信息。
 * @author ZhuHJay
 * @date 2023/1/11 15:37
 */
@Slf4j
public class BasicTokenVerifyFilter extends BasicAuthenticationFilter {

    private final StringRedisTemplate redisTemplate;

    {
        redisTemplate = SpringContextHolder.getBean(StringRedisTemplate.class);
    }

    public BasicTokenVerifyFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 只有登录成功后才会执行到该Filter过滤器中, 进行token有效校验
        String authorization = request.getHeader(AUTH_HEADER_KEY);
        if (StrUtil.isBlank(authorization) || !authorization.startsWith(SecurityConstant.AUTH_REQ_TITLE)) {
            ServletUtils.writeToJson(response, ResultBean.fail(HttpCode.LOGIN_FAILURE));
            return;
        }
        // 判断token是否有效(redis中失效的即token失效)
        String token = JwtUtils.exportToken(authorization);
        String value = redisTemplate.opsForValue().get(TOKEN_PREFIX + token);
        if (value == null) {
            // token失效
            ServletUtils.writeToJson(response, ResultBean.fail(HttpCode.LOGIN_FAILURE));
            return;
        }

        // 根据token中的uid(value可以等价于token解析后的uid)获取到用户信息
        LoginUserDetail loginUserDetail = JacksonUtils.readValue(
                redisTemplate.opsForValue().get(UID_PREFIX + value),
                LoginUserDetail.class);

        // 放到SpringSecurity上下文对象中, 以便该请求控制器中使用
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(
                        loginUserDetail, null,
                        loginUserDetail.getPermissions().stream()
                                .map(SimpleGrantedAuthority::new).collect(Collectors.toList())));

        // 续签
        renew(response, token, loginUserDetail);
        chain.doFilter(request, response);
    }

    /** 续签 **/
    private void renew(HttpServletResponse response, String token, LoginUserDetail loginUserDetail) {
        // 判断是否需要续签
        if (!JwtUtils.isRenew(loginUserDetail)) {
            return;
        }
        String tokenKey = TOKEN_PREFIX + token;
        String uidKey = UID_PREFIX + loginUserDetail.getUid();
        // 获取该token锁
        String lockKey = TOKEN_PREFIX + RedisConstant.LOCK_PREFIX + token;
        // -1: 将要过期; 0: 有线程正在续签; 1: 正常
        int stat = loginUserDetail.getRenewStatus();
        // 这块将会被锁住执行
        if (stat == 1 && RedisUtils.tryLock(lockKey, 30, TimeUnit.SECONDS)) {
            // 再次判断状态是否为1
            LoginUserDetail check = JacksonUtils.readValue(
                    redisTemplate.opsForValue().get(uidKey),
                    LoginUserDetail.class);
            if (check.getRenewStatus() == 1) {
                // 1. 将uid状态设置为0, 表示有线程正在续签
                loginUserDetail.setRenewStatus(0);
                redisTemplate.opsForValue().set(uidKey, JacksonUtils.writeValueAsString(loginUserDetail), 0);

                // 2. 创建新的token
                String newToken = JwtUtils.createToken(loginUserDetail);
                String newTokenKey = TOKEN_PREFIX + newToken;
                String newUidKey = UID_PREFIX + loginUserDetail.getUid();

                // 3. 将新的token存入redis
                redisTemplate.opsForValue().set(newTokenKey, String.valueOf(loginUserDetail.getUid()),
                        EXPIRATION_TIME, TIME_UNIT);
                loginUserDetail.setRenewStatus(1);
                redisTemplate.opsForValue().set(newUidKey, JacksonUtils.writeValueAsString(loginUserDetail),
                        EXPIRATION_TIME, TIME_UNIT);

                // 4. 将新的token响应给客户端，及时进行token的使用更新
                response.addHeader(AUTH_HEADER_KEY, newToken);

                // 5. 续签结束，将旧的token状态设置为-1, 表示将要过期
                loginUserDetail.setRenewStatus(-1);
                redisTemplate.expire(tokenKey, 30, TimeUnit.SECONDS);
                redisTemplate.opsForValue().set(uidKey, JacksonUtils.writeValueAsString(loginUserDetail), 30, TimeUnit.SECONDS);
            }
        }
    }
}
